// Copyright 2012 Ray Lambert <codemonkey@interthingy.net>
// Licensed under GPLv3+ or later (see COPYING)
// hosted at: http://code.google.com/p/argent/

// vim:aw
// se makeprg=bjam

#define VERSION "1.3.0 (2012-03-31)"

#include <iostream>
#include <string>
#include <sstream>
#include <fstream>
#include <vector>
#include <map>
#include <deque>
#include <cstring>
#include <cstdio>
#include <cctype>
#include <stdexcept>
#include <cassert>
#include <sys/stat.h>
#include <sys/types.h>
#include "regex.h"

using std::cin;
using std::cout;
using std::cerr;
using std::endl;
using std::string;
using std::vector;
using std::map;
using std::deque;
using std::pair;
using std::make_pair;
using std::istream;
using std::ostream;
using std::ifstream;
using std::ofstream;
using std::istringstream;
using std::ostringstream;
using std::exception;
using std::invalid_argument;
using std::runtime_error;

static const char whitespace[] = " \t\n";

string trim(const string &s)
{
    string::size_type beg = s.find_first_not_of(whitespace);
    string::size_type end = s.find_last_not_of(whitespace);
    if (beg == string::npos) beg = 0;
    if (end == string::npos) end = s.length(); else ++end;
    if ( beg > 0 or end < s.length() )
        return s.substr(beg,end-beg);
    return s;
}

void split(const string &s, vector<string> &v, char delim=' ')
{
    string::size_type beg = s.find_first_not_of(whitespace);
    if ( beg == string::npos )
    {
        v.push_back(s);
        return;
    }
    while( beg != string::npos and beg < s.length() )
    {
        string::size_type end = s.find_first_of(delim, beg+1);
        if (end != string::npos)
        {
            v.push_back( trim( s.substr(beg,end-beg) ) );
            beg = s.find_first_not_of(delim, end+1);
        }
        else
        {
            v.push_back( trim( s.substr(beg) ) );
            break;
        }
    }
}

string replace(const string &original, const string &s1, const string &s2)
{
    string s = original;
    string::size_type i = 0;
    while( i < s.length() )
    {
        i = s.find(s1,i);
        if ( i == string::npos )
            break;
        s.replace(i, s1.length(), s2);
        i += s2.length();
    }
    return s;
}

template<typename T> T tonum(const string &s)
{
    std::istringstream is(s);
    is.unsetf(std::ios::basefield); // determine number base automatically
    T v(0);
    is >> std::boolalpha >> std::ws >> v >> std::ws;
    if ( is.bad() or is.fail() or not is.eof() )
        throw std::invalid_argument(s);
    return v;
}

template<typename T> bool valid_number(const string &s)
{
    try
    {
        tonum<T>(s);
        return true;
    }
    catch(...)
    {
    }
    return false;
}

bool exists(const string &fn)
{
    struct stat st;
    int rc = ::stat(fn.c_str(), &st);
    return rc == 0;
}

////////////////////////////////////////////////////////////////////////////////

class options
{
public:
    typedef unsigned value_t;
    value_t opts;
    map<string,string> values;
    vector<string> names;
    options(value_t v=0)            : opts(v) {}
    options(options &copy)          : opts(copy.opts), values(copy.values) {}
    void operator+=(value_t v)      { opts |= v; }
    void operator-=(value_t v)      { opts &= ~v; }
    bool has(value_t v) const       { return opts & v; }
    bool hasnot(value_t v) const    { return (opts & v) == 0; }
    bool has_all(value_t v) const   { return (opts & v) == v; }
};

static const options::value_t verbose = 0x001;
static const options::value_t linenos = 0x002;
static const options::value_t preview = 0x004;
static const options::value_t backups = 0x008;
static const options::value_t csvfrmt = 0x010;
static const options::value_t ignorec = 0x020;
static const options::value_t context = 0x040;
static const options::value_t singlel = 0x080;
static const options::value_t oformat = 0x100;
static const options::value_t findall = 0x200;
static const options::value_t usegrps = 0x400;
static const options::value_t varfrmt = 0x800;

////////////////////////////////////////////////////////////////////////////////

void help()
{
    cout <<
"rex " VERSION "\n\
\n\
Usage: rex [options] <exp> [<inputs>]\n\
\n\
Rex is a tool for processing text using regular expressions.  Two specific\n\
tools are provided: a substitution tool (similar to rpl or sed and vi's\n\
's///' (subst) commands) and a parsing tool which can be used to parse fields\n\
out of an input string and format them (similar to cut but using regular\n\
expressions).  Extended regular expressions are used in all cases.\n\
\n\
Rex processes single lines of text from either files, strings (given on the\n\
command line) or stdin.  One or more regular files can be listed on the\n\
command line or one or more strings can be specified using the -s switch (but\n\
the two cannot be mixed).  If neither are given standard input is processed.\n\
\n\
Common Options:\n\
\n\
  -s <string>\n\
\n\
      A string to process.  The -s option may be specified multiple times and\n\
      each is treated as a separate line of input (in the order listed on the\n\
      command line).  This option may not be combined with files or stdin.\n\
\n\
  --all,-a\n\
\n\
      Find all matches on each line.  The pattern search is repeated until the\n\
      entire line is exhausted.  By default searching stops after the first\n\
      match on each line.\n\
\n\
  --ignore-case,-i\n\
\n\
      Ignore alphabetic case in regular expressions.\n\
\n\
  --verbose,-v\n\
\n\
      Display verbose output.  Note that this may be counter-productive when\n\
      processing stdin.\n\
\n\
Substitution Tool\n\
\n\
    The substitution tool applies a replacement string, specified with the\n\
    --subst switch, to each line that matches the given regular expression.\n\
    The replacement string can have group references as well as certain\n\
    backslash character translations and operators.\n\
\n\
    All processed lines are printed to stdout or a file, regardless of\n\
    whether they were effected by substitutions or not.  If the input is\n\
    stdin or one or more strings the lines are printed to stdout.  If the\n\
    input is a file, the file is re-written with the new text.  If the\n\
    --backup switch is used, the original file is renamed with a tilde\n\
    appended to its file name.\n\
\n\
    If the --preview switch is used, rex instead produces output on stdout in\n\
    a format similar to diff: the original line is prefixed with a minus sign\n\
    and the replacement line is prefixed with a plus sign along with file\n\
    names and line numbers (when these apply).\n\
\n\
Options Specific to the Substitution Tool:\n\
\n\
    --subst,--rep,-r <string>\n\
\n\
        Enables the Substitution Tool and defines the replacement string.\n\
        All matches on all input lines will be substituted by the expansion\n\
        of <string>.  <string> may contain group references, backslash\n\
        character translations and operators (see below).\n\
\n\
        When substitutions are performed on a file, the file is re-written\n\
        with the substitutions and the original file is overwritten (a backup\n\
        can be made using the --backup switch).\n\
\n\
        When substitutions are performed on stdin or on strings specified\n\
        using the -s switch, the results are written to stdout.\n\
\n\
        If --verbose is used, all substituted lines are written to stdout\n\
        using a diff-like format.  (Note that this is counter-productive with\n\
        stdin.)\n\
\n\
        If --preview is used, all would-be substituted lines are written to\n\
        stdout using a diff-like format and no changes are actually made.\n\
\n\
        <string> may contain group references (sometimes called 'back\n\
        references').  These consist of a backslash followed by a single digit.\n\
        The digits 1-9 refer to groups in the regular expression, in the order\n\
        that they appear, and are substituted by such.  Any given group may\n\
        match (and subsequently substitute) a blank string but it is an error\n\
        to refer to a group that does not exist.  The digit 0 refers to the\n\
        entire match (note that some tools use the ampersand for this purpose\n\
        but rex does not recognize it).  If --all is used, group references\n\
        refer individually to each single match of the regular expression\n\
        (rather than the entire line, in the case of multiple matches in a\n\
        single line).\n\
\n\
        <string> may contain certain operators that do not themselves insert\n\
        any text but which effect how other text is inserted.  Presently the\n\
        uppercase (\\U), lowercase (\\L) and word case (\\W) operators are\n\
        supported: they will cause the case of the next group substitution to\n\
        be modified accordingly.\n\
\n\
        <string> may contain certain backslash character translations, as\n\
        shown in this table:\n\
\n\
            \\n     newline\n\
            \\r     carriage-return\n\
            \\t     tab\n\
            \\f     form feed\n\
            \\b     backspace\n\
            \\z     ASCII 0\n\
\n\
        Any character in <string> that is preceeded by a backslash but is not\n\
        identified above is output verbatim without the backslash.  To insert\n\
        a literal backslash a double backslash must be used.  A backslash at\n\
        the end of a line is output literally.\n\
\n\
    --preview,-p\n\
\n\
        Do not actually make any changes.  Would-be substitutions are\n\
        displayed using a diff-like format.\n\
\n\
    --backup,-b\n\
\n\
        When performing a substitution on a file, make a backup of the file.\n\
        Files are backed-up by renaming them with a tilde (~) on the end.  If\n\
        a file with the same name as the backup file already exists the\n\
        corresponding input file will not be processed (a warning is issued).\n\
\n\
    --context-lines,-c <num-lines>\n\
\n\
        Enable the display of context lines when previewing a set of\n\
        substitutions and specify the number of context lines to display.\n\
        Context lines are the unchanged lines that surround the line or lines\n\
        that contain a substitution.  <num-lines> indicates the number of\n\
        context lines to display both before and after the changed lines.\n\
\n\
Parse Tool\n\
\n\
    The parse tool searches each input line for one or more matching\n\
    sub-strings (or 'fields').  Normally the regular expression is searched\n\
    for once on each line but if --all is used the line is searched repeatedly\n\
    until the line is exhausted.  By default, each matching field is output on\n\
    a seperate line but other formats may be selected by specifying options\n\
    (described below).  If --groups is used (and the regular expression contains\n\
    groups) only the matching groups are output.  Note that --all and --groups\n\
    have a significant effect on exactly what gets parsed and output.\n\
\n\
    Note that all of the formatting options (described below) are mutually-\n\
    exclusive (i.e. only one can be used per invocation).\n\
\n\
Options Specific to the Parse Tool:\n\
\n\
    --groups,-g\n\
\n\
        Output only the matching sub-groups instead of the whole match.\n\
\n\
    --csv\n\
\n\
        Enables output to be formatted like a CSV file.  The results for each\n\
        matching input line are printed on a single line with the fields\n\
        separated by commas.  Any field that contains a comma is written in\n\
        double quotes (embedded quotes are doubled, as per CSV standards).\n\
        A simple example:\n\
\n\
            $ rex --all '[a-z]+' --csv -s '--cat--bat--rat'\n\
            cat,bat,rat\n\
\n\
    --names,-n <name>[,<name> ...]\n\
\n\
        Defines a list of names that is used to augment the results.  If\n\
        --names is used in conjunction with --csv the names are output on the\n\
        first line and serve as the CSV header defining the column names.\n\
        Otherwise, --names enables the shell-variable-like output format.\n\
\n\
        In this format, each <name> is printed in sequence followed by an\n\
        equals sign ('=') and the corresponding matching text.  Ideally, the\n\
        number of names given is equal to the number of parsed fields.  If\n\
        there are fewer names than fields the extra fields are not printed.\n\
        If there are more names than fields the extra names are printed with a\n\
        blank string (\"\") after the equals sign.  The intention is to produce\n\
        a set of definitions formatted like shell variables which may be\n\
        'sourced' by a shell script.\n\
        \n\
        A simple example:\n\
\n\
            $ rex --all '[a-z]+' --names 'CAT,BAT,RAT' -s '--cat--bat--rat'\n\
            CAT=\"cat\"\n\
            BAT=\"bat\"\n\
            RAT=\"rat\"\n\
\n\
        An example using groups:\n\
\n\
            $ rex --groups '([a-z]+)--([a-z]+)--' --names 'CAT,BAT,RAT' \\\n\
                -s '--cat--bat--rat'\n\
            CAT=\"cat\"\n\
            BAT=\"bat\"\n\
            RAT=\"\"\n\
\n\
        A CSV example:\n\
\n\
            $ rex --all '[a-z]+' --csv --names 'CAT,BAT,RAT' -s '--cat--bat--rat'\n\
            CAT,BAT,RAT\n\
            cat,bat,rat\n\
\n\
    --single-line,-1\n\
\n\
        Causes all matches from the same line to be output on a single line\n\
        separated by spaces.\n\
\n\
    --format,-f <output-format>\n\
\n\
        Defines a custom format for outputting parsed fields.  <output-format>\n\
        works exactly like a replacement string, as used in substitution mode\n\
        (see --subst), except that only the matching text is output and no\n\
        error is generated for undefined group references, which expand to a\n\
        blank string (this allows for partial matches on some lines).  When a\n\
        custom format is in use, the matches are output exclusively by way of\n\
        the expanded <output-format> string.\n\
\n\
        A simple example:\n\
\n\
            $ rex --all '[a-z]+' --format '\\3:\\U\\2:\\1' -s '--cat--bat--rat--'\n\
            rat:BAT:cat\n\
";
}

////////////////////////////////////////////////////////////////////////////////

void parse_stream(
    const regex &exp, 
    istream &is, 
    options &opts);

long subst_stream(
    const regex &exp, 
    const std::string &rep, 
    istream &is, 
    ostream &os, 
    options &opts, 
    const string &fname="");

int main(int argc, char *argv[])
{
    string exp, rep;
    vector<string> inputs;
    options opts;
    enum { none, files, strings } what = none;
    enum { parse, subst } mode = parse;

    try
    {
        for(int i=1; i<argc; ++i)
        {
            string arg( argv[i] );
            if ( arg == "-h" or arg == "--help" )
            {
                help();
                return 1;
            }
            else if ( arg == "-s" or arg == "--string" )
            {
                if ( ++i >= argc )
                    throw invalid_argument("missing value on " + arg + " option");
                if ( what == files )
                    throw invalid_argument("file names and string inputs cannot be mixed");
                what = strings;
                inputs.push_back( argv[i] );
            }
            else if ( arg == "--rep" or arg == "--subst" or arg == "-r" )
            {
                if ( ++i >= argc )
                    throw invalid_argument("missing value on " + arg + " option");
                if ( not rep.empty() )
                    throw invalid_argument(arg + " used more than once");
                rep = argv[i];
                mode = subst;
            }
            else if ( arg == "-n" or arg == "--names" )
            {
                if ( ++i >= argc )
                    throw invalid_argument("missing value on " + arg + " option");
                if ( not opts.names.empty() )
                    throw invalid_argument(arg + " used more than once");
                string s( argv[i] ); 
                split(s,opts.names,',');
                if ( opts.names.empty() )
                    cerr << "warning: no names could be parsed for " << arg
                        << " option: '" << s << '\'' << endl;
            }
            else if ( arg == "--csv" )
            {
                opts += csvfrmt;
            }
            else if ( arg == "-a" or arg == "--all" )
            {
                opts += findall;
            }
            else if ( arg == "-i" or arg == "--ignore-case" )
            {
                opts += ignorec;
            }
            else if ( arg == "-v" or arg == "--verbose" )
            {
                opts += verbose;
            }
            else if ( arg == "-p" or arg == "--preview" )
            {
                opts += preview;
            }
            else if ( arg == "-1" or arg == "--single-line" )
            {
                opts += singlel;
            }
            else if ( arg == "-g" or arg == "--groups" )
            {
                opts += usegrps;
            }
            else if ( arg == "-b" or arg == "--backup" )
            {
                opts += backups;
            }
            else if ( arg == "-c" or arg == "--context-lines" )
            {
                if ( ++i >= argc )
                    throw invalid_argument("missing value on " + arg + " option");
                opts += context;
                string s( argv[i] );
                if ( not valid_number<int>(s) and tonum<int>(s) < 1 )
                    throw invalid_argument("invalid numeric value on " + arg + " option: " + s);
                opts.values["context-lines"] = s;
            }
            else if ( arg == "-f" or arg == "--format" )
            {
                if ( ++i >= argc )
                    throw invalid_argument("missing value on " + arg + " option");
                opts += oformat;
                string s = argv[i];
                if ( s.empty() )
                    throw invalid_argument("format string cannot be blank");
                opts.values["output-format"] = s;
            }
            else
            {
                if ( exp.empty() )
                {
                    exp = arg;
                }
                else
                {
                    if ( what == strings )
                        throw invalid_argument("file names and string inputs cannot be mixed");
                    what = files;
                    inputs.push_back(arg);
                }
            }
        }

        // if names were given but --csv was not, enable varfrmt implicitly
        if ( not opts.names.empty() and not opts.has(csvfrmt) ) opts += varfrmt;

        // count selected output formats; must be zero or one
        int count = 0;
        if ( opts.has(varfrmt) ) ++count;
        if ( opts.has(csvfrmt) ) ++count;
        if ( opts.has(singlel) ) ++count;
        if ( opts.has(oformat) ) ++count;
        if ( count > 1 )
            throw invalid_argument("more than one output format was specified");

        // give warnings for effectless options
        if ( mode == parse )
        {
            if ( opts.has(backups) )
                cerr << "warning: --backup option has no effect on parsing" << endl;
            if ( opts.has(preview) )
                cerr << "warning: --preview option has no effect on parsing" << endl;
            if ( opts.has(context) )
                cerr << "warning: --context-lines option has no effect on parsing" << endl;
        }
        else
        {
            if ( opts.has(csvfrmt) )
                cerr << "warning: --csv option has no effect when substituting" << endl;
            else // don't give --names warning when --csv given since they go together
                if ( not opts.names.empty() )
                    cerr << "warning: --names option has no effect when substituting" << endl;
            if ( opts.has(singlel) )
                cerr << "warning: --single-line option has no effect when substituting" << endl;
            if ( opts.has(oformat) )
                cerr << "warning: --format option has no effect when substituting" << endl;
            if ( opts.has_all(backups|preview) )
                cerr << "warning: --backup option has no effect when previewing" << endl;
        }

        // prepare the regex
        if ( exp.empty() )
            throw invalid_argument("no regular expression specified");
        unsigned flags = regex::extended;
        if ( opts.has(ignorec) )
            flags |= regex::nocase;
        regex rexp(exp,flags);

        // process inputs
        if ( opts.has(preview) )
            cout << "**previewing**" << endl;
        switch( what )
        {
            case none:
            {
                // process stdin
                opts += linenos;
                if ( mode == parse )
                    parse_stream(rexp, cin, opts);
                else
                    subst_stream(rexp, rep, cin, cout, opts);
                break;
            }

            case strings:
            {
                // process strings
                for(auto &line : inputs)
                {
                    istringstream is(line);
                    if ( mode == parse )
                        parse_stream(rexp, is, opts);
                    else
                        subst_stream(rexp, rep, is, cout, opts);
                }
                break;
            }

            case files:
            {
                // process files
                opts += linenos;
                for(auto &fn : inputs)
                {
                    ifstream is(fn);
                    if ( is.fail() )
                    {
                        cerr << "error: file not found: " << fn << endl;
                        continue;
                    }
                    if ( mode == parse )
                    {
                        if ( opts.has(verbose|preview) )
                            cout << "== file " << fn << ":" << endl;
                        parse_stream(rexp, is, opts);
                    }
                    else
                    {
                        string bak = fn + "~";
                        string tmp = fn + "~~";
                        if ( opts.has(backups) and opts.hasnot(preview) and exists(bak) )
                        {
                            cerr << "warning: backup file already exists: " << bak << endl;
                            cerr << "warning: skipping input file: " << fn << endl;
                            continue;
                        }
                        ofstream os;
                        if ( opts.hasnot(preview) )
                        {
                            os.open(tmp);
                            if ( os.fail() )
                            {
                                cerr << "error: cannot create temporary file: " << tmp << endl;
                                cerr << "warning: skipping input file: " << fn << endl;
                                continue;
                            }
                        }
                        long changes = subst_stream(exp, rep, is, os, opts, fn);
                        if ( opts.hasnot(preview) )
                        {
                            os.close();
                            if ( changes > 0 )
                            {
                                if ( opts.has(backups) )
                                    std::rename( fn.c_str(), bak.c_str() );
                                else
                                    std::remove( fn.c_str() );
                                std::rename( tmp.c_str(), fn.c_str() );
                            }
                            else
                            {
                                // just remove the temp file if no changes were
                                // made (so we don't touch the timestamp)
                                std::remove( tmp.c_str() );
                            }
                        }
                    }
                }
            }
        }
    }
    catch(const exception &e)
    {
        cerr << "error: " << e.what() << endl;
        return 2;
    }

    return 0;
}

////////////////////////////////////////////////////////////////////////////////

void output_fields(
    options &opts, 
    const string &line,
    long lineno, 
    const vector<string> &fields)
{
    if ( opts.has_all(verbose|linenos) )
        cout << "# line " << lineno << ":\n";
    if ( opts.has(csvfrmt) )
    {
        // CVS output
        bool first = true;
        for(auto &field : fields)
        {
            if ( first ) first = false; else cout << ',';
            if ( field.find(',') != string::npos )
            {
                cout << '\"' << replace(field,"\"","\"\"") << '\"';
            }
            else
            {
                cout << field;
            }
        }
        cout << endl;
    }
    else if ( opts.has(singlel) )
    {
        // single line output
        bool first = true;
        for(auto &field : fields)
        {
            if ( first ) first = false; else cout << ' ';
            cout << field;
        }
        cout << endl;
    }
    else if ( opts.has(oformat) )
    {
        string ns = regex::subst( line, fields, opts.values["output-format"] );
        cout << ns << endl;
    }
    else if ( opts.has(varfrmt) )
    {
        // shell-var-like output
        assert( not opts.names.empty() );
        for(unsigned i=0; i < fields.size() and i < opts.names.size(); ++i)
        {
            cout << opts.names[i] << "=\""
                << replace(fields[i], "\"", "\\\"")
                << '\"' << endl;
        }
        for(unsigned i=fields.size(); i < opts.names.size(); ++i)
            cout << opts.names[i] << "=\"\"" << endl;
    }
    else
    {
        // plain output
        for(auto &field : fields)
            cout << field << endl;
    }
}

void parse_stream(
    const regex &exp, 
    istream &is, 
    options &opts)
{
    try
    {
        // if outputting csv format and names are given, output the csv header now
        if ( opts.has(csvfrmt) and not opts.names.empty() )
        {
            bool first = true;
            for(auto &name : opts.names)
            {
                if ( first ) first = false; else cout << ',';
                cout << name;
            }
            cout << endl;
        }

        string line;
        long lineno = 0;
        while( getline(is,line) )
        {
            ++lineno;
            vector<string> fields;
            regex::results res;
            string::size_type i = 0;
            int so = line.length(), eo = 0;
            if ( opts.has(oformat) )
                fields.push_back(""); // reserve space for \0 reference
            unsigned count = 0;
            while( i < line.length() )
            {
                if ( not exp.exec(line,res,i) )
                    break;
                // build the matches
                vector<string> matches;
                for(auto &match : res)
                {
                    if ( opts.has(oformat) )
                    {
                        if ( match.rm_so < so )
                            so = match.rm_so;
                        if ( match.rm_eo > eo )
                            eo = match.rm_eo;
                    }
                    matches.push_back( line.substr(match.rm_so, match.rm_eo - match.rm_so) );
                }
                if ( opts.has(usegrps) )
                {
                    for(unsigned i=1; i < matches.size(); ++i)
                    {
                        fields.push_back( matches[i] );
                        ++count;
                    }
                }
                else
                {
                    fields.push_back( matches[0] );
                    ++count;
                }
                if ( not opts.has(findall) )
                    break;
                i = res[0].rm_eo;
            }
            if ( count > 0 )
            {
                if ( opts.has(oformat) )
                {
                    fields[0] = line.substr(so,eo-so); // \0 reference
                    // in order to support formatting in the case where the
                    // number of matches may not equal the maximum number of
                    // matches, we'll pad out the fields array with blank
                    // strings, up to ten (the maximum supported number of group
                    // references)
                    while( fields.size() < 10 )
                        fields.push_back("");
                }
                output_fields(opts, line, lineno, fields);
            }
        }
    }
    catch(const exception &e)
    {
        cerr << "error: " << e.what() << endl;
    }
}

////////////////////////////////////////////////////////////////////////////////

long subst_stream(
    const regex &exp, 
    const string &rep, 
    istream &is, 
    ostream &os, 
    options &opts, 
    const string &fname/*=""*/)
{
    unsigned num_context = 0;
    if ( opts.has(context) )
        num_context = tonum<unsigned>( opts.values["context-lines"] );
    deque<string> context_lines;
    enum { pre, post } state = pre;
    try
    {
        string s;
        long line = 0;
        long changes = 0;
        regex::results res;
        while( getline(is,s) )
        {
            ++line;
            ostringstream rs;
            string::size_type i = 0;
            while( i < s.length() )
            {
                if ( exp.exec(s,res,i) )
                {
                    if ( res[0].rm_so > int(i) )
                        rs << s.substr(i, res[0].rm_so - i);
                    rs << exp.subst(s,res,rep,false);
                    if ( not opts.has(findall) )
                    {
                        rs << s.substr( res[0].rm_eo );
                        break;
                    }
                    i = res[0].rm_eo;
                }
                else
                {
                    rs << s.substr(i);
                    break;
                }
            }
            string ns = rs.str();
            bool changed = ns != s;
            if ( opts.has(verbose|preview) and changed )
            {
                if ( changes == 0 and not fname.empty() )
                    cout << "== file " << fname << ":" << endl;
                if ( num_context )
                {
                    for(auto &line : context_lines)
                        cout << "  " << line << endl;
                    context_lines.clear();
                    state = post;
                }        
                if ( opts.has_all(verbose|linenos) )
                    cout << "## line " << line << ":\n";
                cout << "- " << s << endl;
                cout << "+ " << ns << endl;
            }
            if ( not changed and num_context )
            {
                if ( state == post and context_lines.size() == num_context )
                {
                    for(auto &line : context_lines)
                        cout << "  " << line << endl;
                    state = pre;
                }
                context_lines.push_back(s);
                while( context_lines.size() > num_context )
                    context_lines.pop_front();
            }
            if ( opts.hasnot(preview) )
                os << ns << endl;
            if ( changed )
                ++changes;
        }
        if ( num_context )
            for(auto &line : context_lines)
                cout << "  " << line << endl;
        return changes;
    }
    catch(const exception &e)
    {
        cerr << "error: " << e.what() << endl;
    }
    return 0;
}
